<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>vue 原理 （累加器例子）</title>
  </head>
  <body>
    <div id="app"></div>
    <script>
      /* ======== 渲染函数生成 virtual dom ======== */
      function h(tag, attrs, children) {
        return {
          tag,
          attrs: attrs || {},
          children: (children || []).map((child) =>
            typeof child === "string"
              ? { tag: "text", attrs: {}, children: [child] }
              : child
          ),
        };
      }

      /* ======== Vue 组件，是个对象 ======== */
      const app = {
        data() {
          return {
            num: 0,
          };
        },
        watch: {
          num() {
            console.log("num changed");
          },
        },
        render() {
          return h("div", {}, [
            h("h1", {}, [this.num + ""]),
            h("button", { onClick: this.add }, ["add"]),
          ]);
        },
        methods: {
          add() {
            this.num++;
          },
        },
      };
      // @beforeCreate

      /* ======== 新旧 virtual dom 进行比对和渲染 ======== */
      //pre 之前的虚拟dom
      let pre = {};
      //now 当前的虚拟dom
      let now = {};
      const root = document.querySelector("#app");
      //parent 父节点 preNode 之前的虚拟dom  nowNode 当前的虚拟dom 只会同级比较，递归
      //diff函数的作用是比较新旧虚拟dom的差异，然后更新到真实dom上
      //  diff(root, pre, now);
      function diff(parent, preNode, nowNode) {
        // 第一次渲染start
        if (!preNode.tag && nowNode.tag) {
          // 新增
          if (nowNode.tag === "text") {
            // 处理文本节点
            const node = document.createTextNode(nowNode.children[0]);
            parent.appendChild(node);
            //给旧的虚拟dom pre 赋值，方便下次比较
            preNode.tag = nowNode.tag;
            preNode.attrs = nowNode.attrs;
            preNode.children = nowNode.children;
            preNode.node = node;
          } else {
            // 处理普通节点
            const node = document.createElement(nowNode.tag); //nowNode.tag标签名
            //nowNode.attrs 属性对象
            Object.keys(nowNode.attrs).forEach((key) => {
              if (key === "onClick") {
                node.onclick = nowNode.attrs[key];
              } else {
                node.setAttribute(key, nowNode.attrs[key]);
              }
            });
            parent.appendChild(node);
            //给旧的虚拟dom pre 赋值，方便下次比较
            preNode.tag = nowNode.tag;
            preNode.attrs = nowNode.attrs;
            preNode.node = node;
            preNode.children = [];
            nowNode.children.forEach((child) => {
              const childNode = {};
              preNode.children.push(childNode);
              diff(node, childNode, child);
            });
          }         
        } // 第一次渲染end
        // 删除start
        else if (preNode.tag && !nowNode.tag) {
          parent.removeChild(preNode.node);
          // 清空 pre 中的数据,有问题！！！！！！
          Object.assign(preNode, {});
        } 
        // 删除end
        // 更新start
        else {
          // 如果根节点不同更新
          if (preNode.tag !== nowNode.tag) {
            // 新旧节点不同，直接替换
            //q:下面的代码什么意思?
            //a:先删除旧的节点，再创建新的节点
            diff(parent, preNode, {});
            diff(parent, {}, nowNode);
            return;
          }
          // 如果根节点相同，更新属性
          Object.keys(nowNode.attrs).forEach((key) => {
            if (preNode.attrs[key] !== nowNode.attrs[key]) {
              if (key === "onClick") {
                preNode.node.onclick = nowNode.attrs[key];
              } else {
                preNode.node.setAttribute(key, nowNode.attrs[key]);
              }
            }
          });
          //更新属性end
          // 更新完属性后，更新 preNode，方便下次比较
          preNode.attrs = nowNode.attrs;
          // 处理文本节点start
          if (nowNode.tag === "text") {
            const content = nowNode.children[0];
            if (preNode.children[0] !== content) {
              preNode.node.textContent = content;
              preNode.children = [content];
            }
          } 
          //处理文本节点end
          else {
            // children 中的节点 key 问题（当前演示 index）
            const usedKeys = [];
            preNode.children.forEach((_, key) => {
              usedKeys.push(key);
              if (!nowNode.children[key]) {
                // 处理 children 删除
                diff(preNode.node, preNode.children[key], {});
                preNode.children.splice(key, 1);
              } else {
                // 处理 children 更新
                diff(
                  preNode.node,
                  preNode.children[key],
                  nowNode.children[key]
                );
              }
            });
            nowNode.children.forEach((_, key) => {
              if (!usedKeys.includes(key)) {
                // 处理 children 新增
                const child = {};
                preNode.children.push(child);
                diff(preNode.node, child, nowNode.children[key]);
              }
            });
          }
        }
        // 更新end
      }

      /* ======== 挂载 watch this 生成 Vue 实例 ======== */
      app.watchList = [];
      const appData = app.data();
      const config = {};

      /* ======== Object.defineProperties 响应式 ======== */
      Object.keys(appData).forEach((key) => {
        config[key] = {
          get() {
            // 进入 watch 队列
            if (!app.watchList.includes(key)) {
              app.watchList.push(key);
            }
            return appData[key];
          },
          set(value) {
            appData[key] = value;
            Promise.resolve().then(() => {
              now = app.render();
              // @beforeUpdate
              diff(root, pre, now);
              // @updated
              // diff 完成后，启动监听
              app.watchList.forEach((key) => {
                app.watch[key] && app.watch[key]();
              });
            });
          },
        };
      });
      const proto = Object.defineProperties({}, config);
      for (method in app.methods) {
        proto[method] = app.methods[method].bind(
          Object.defineProperties({}, config)
        );
      }
      app.render = app.render.bind(proto);
      // @created/@beforeMount

      /* ======== 首次运行 ======== */
      now = app.render();
      console.log(now)
      
      diff(root, pre, now);
      console.log(pre)
      console.log(now)
      
      
      // @mounted
    </script>
  </body>
</html>

